// HttpServer.java - A multithreaded HTTP server.
//
// Copyright (C) 1999-2002  Smart Software Consulting
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
//
// Smart Software Consulting
// 1688 Silverwood Court
// Danville, CA  94526-3079
// USA
//
// http://www.smartsc.com
//

package com.smartsc.http;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.IOException;
import java.io.PrintWriter;

import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.net.URL;

import java.util.Date;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Vector;
import java.util.StringTokenizer;

import javax.servlet.RequestDispatcher;
import javax.servlet.Servlet;
import javax.servlet.ServletConfig;
import javax.servlet.ServletContext;

import com.smartsc.util.AbsoluteFile;
import com.smartsc.util.Mailer;
import com.smartsc.util.NVPairParser;
import com.smartsc.util.Properties;

public class
HttpServer
implements ServletContext, HttpStatusCodes, HttpReasonPhrases, Runnable
{
	public
	HttpServer( Properties props)
	throws IOException
	{
		this.props = props;

		// Get verbosity from properties
		boolean verbose = props.getBooleanProperty( PROP_VERBOSE);

		openLog( true );
		openTransferLog( true );

		// Log startup message
		banner( verbose);

		// Setup handler counter
		maxHandlers = props.getIntProperty(
			PROP_MAX_HANDLERS, DEFAULT_MAX_HANDLERS);
		handlerCount = 0;

		int backlog = props.getIntProperty( PROP_BACKLOG, -1);
		int port = props.getIntProperty( PROP_PORT, DEFAULT_PORT);
		if( port == 0)
			port = DEFAULT_PORT;

		if( backlog > 0)
		{
			serverSocket = new ServerSocket( port, backlog);
		}
		else
		{
			serverSocket = new ServerSocket( port);
		}

		StringBuffer sb = new StringBuffer(
			InetAddress.getLocalHost().getHostName());
		sb.append( ":");
		sb.append( port);
		serverName = sb.toString();

		// Parse servlet context init params
		String initParams = 	props.getProperty( PROP_INIT_PARAMS);
		if( initParams != null)
		{
			new NVPairParser( initParams, ",", "=")
			{
				public void forEachNVPair( String name, String value)
				{
					servletContextInitParams.put( name.trim(), value);
				}
			}.parse();
		}

		// Load servlets properties file
		String servletPropFile = props.getProperty(
			PROP_SERVLET_PROPS, DEFAULT_SERVLET_PROPS);
		try
		{
			File f = AbsoluteFile.create( servletPropFile);
			servletProperties.load( new FileInputStream( f));
		}
		// Ignore exceptions
		catch( IOException ie) {}

		// Process servlet properties (i.e. .startup and .mappings)
		processServletProperties();
	}

	public
	void
	run()
	{
		Socket clientSocket;

		while( true)
		{
			try
			{
				// If we have max number of handlers
				if( handlerCount >= maxHandlers)
				{
					// Wail until some finish
					synchronized( this)
					{
						while( handlerCount >= maxHandlers)
						{
							try
							{
								wait();
								/* TINI Omit
								// (wait is only a stub in TINI Beta 2.1)
								Thread.sleep( 100);
								//*/
							}
							catch( InterruptedException ie) {}
						}
					}
				}

				clientSocket = serverSocket.accept();
				clientSocket.setSoTimeout(
					1000 * props.getIntProperty( PROP_REQUEST_TIMEOUT));
			}
			catch( IOException ioe)
			{
				log( "Exception in accept()", ioe);
				continue;
			}

			try
			{
				HttpServerHandler handler = createHandler( this, clientSocket);
				new Thread( handler).start();
			}
			catch( HttpException he)
			{
				log( he);
				try
				{
					HttpResponse.sendError(
						clientSocket.getOutputStream(),
						he.getStatusCode(),
						he.getReasonPhrase());
				}
				catch( IOException ioe) {}
			}
			catch( Throwable t)
			{
				log( t);
				try
				{
					HttpResponse.sendError(
						clientSocket.getOutputStream(),
						SC_INTERNAL_SERVER_ERROR,
						RP_INTERNAL_SERVER_ERROR);
				}
				catch( IOException ioe) {}
			}
		}
	}

	protected
	void
	openLog( boolean append )
	{
		String logFileName = props.getProperty( PROP_LOG_FILE);

		if( logFileName != null)
		{
			try
			{
				logFile = AbsoluteFile.create( logFileName);
				logFileName = logFile.getAbsolutePath();
				logWriter = new PrintWriter(
					new FileOutputStream( logFileName, append ), true );
			}
			catch( IOException ioe)
			{
				System.err.println(
					"Could not open log file '" +
					logFileName + "' for writing.");
				logWriter = new PrintWriter( System.out, true);
				logFile = null;
			}
		}
		else
		{
			logWriter = new PrintWriter( System.out, true);
			logFile = null;
		}
	}

	protected
	void
	openTransferLog( boolean append )
	{
		String logFileName = null;
		if( logFile != null)
		{
			logFileName = logFile.getAbsolutePath();
		}

		String transferLogName = props.getProperty( PROP_TRANSFER_LOG);
		if( transferLogName != null)
		{
			transferLogFile = AbsoluteFile.create( transferLogName);
			transferLogName = transferLogFile.getAbsolutePath();
		}

		// Set transferLogWriter = logWriter if any one of these three
		// conditions is true...
		// 1) server.transferLog is not defined
		// 2) server.transferLog is the same as server.logFile
		// 3) server.transferLog is "-" and logFile is not defined
		if( transferLogName == null	|| transferLogName.equals( logFileName)
		||( transferLogName.equals( "-") && transferLogName == null) )
		{
			transferLogWriter = logWriter;
			transferLogFile = logFile;
		}
		else if( transferLogName.equals( "-"))
		{
			transferLogWriter = new PrintWriter( System.out, true);
			transferLogFile = null;
		}
		else
		{
			try
			{
				transferLogWriter = new PrintWriter(
					new FileOutputStream( transferLogName, append ), true);
			}
			catch( IOException ioe)
			{
				System.err.println(
					"Could not open transfer log file '" +
					transferLogName + "' for writing.");
				transferLogWriter = logWriter;
				transferLogFile = logFile;
			}
		}
	}

	public
	void
	logTransfer( String msg)
	{
		if( transferLogWriter != null)
		{
			synchronized( transferLogWriter)
			{
				transferLogWriter.println( msg);
			}
		}
	}

	protected
	HttpServerHandler
	createHandler( HttpServer server, Socket clientSocket)
	throws HttpException
	{
		return new HttpServerHandler( server, clientSocket);
	}

	protected synchronized
	void
	addHandler()
	{
		++handlerCount;
	}

	protected synchronized
	void
	removeHandler()
	{
		--handlerCount;
		if( (handlerCount == 0 && getTimeSinceLastGC() > MIN_GC_TIME))
		{
			gc();
		}
		notifyAll();
	}

	protected synchronized
	long
	getTimeSinceLastGC()
	{
		return System.currentTimeMillis() - lastGCTime;
	}

	protected synchronized
	void
	gc()
	{
		// Check log file sizes
		if( logFile != null)
		{
			synchronized( logWriter)
			{
				if( isLogOversized( logFile))
				{
					logWriter.close();
					try
					{
						if( isMailingOversizedLogs() )
						{
							mailLog( logFile);
						}
						openLog( false );
					}
					catch( IOException ioe)
					{
						// Truncate log file then log exception
						openLog( false );
						log( ioe );
					}
				}
			}
		}

		if( transferLogFile != null && transferLogFile != logFile)
		{
			synchronized( transferLogWriter)
			{
				if( isLogOversized( transferLogFile))
				{
					transferLogWriter.close();
					if( isMailingOversizedLogs() )
					{
						try
						{
							mailLog( transferLogFile);
						}
						catch( IOException ioe)
						{
							log( ioe );
						}
					}
					openTransferLog( false );
				}
			}
		}

		System.gc();
		lastGCTime = System.currentTimeMillis();
	}

	protected void processServletProperties()
	{
		Enumeration e = servletProperties.keys();
		while( e.hasMoreElements())
		{
			String propName = (String)e.nextElement();
			String propValue = getServletProperty( propName );
			if( propValue == null )
			{
				continue;
			}

			if( propName.endsWith( PRELOAD))
			{
				boolean preload = Boolean.valueOf( propValue).booleanValue();
				if( preload)
				{
					String servletName = propName.substring( 0,
						propName.length() - PRELOAD.length());

					getNamedDispatcher( servletName);
				}
			}
			else if( propName.endsWith( MAPPING))
			{
				String servletName = propName.substring( 0,
					propName.length() - MAPPING.length());

				// If prefix mapping
				if( propValue.startsWith( "/" ) && propValue.endsWith( "/*" ))
				{
					// TODO: warn on duplicate patterns?
					prefixServletMappings.put(
						propValue.substring( 0, propValue.length() - 2 ),
						servletName);
				}
				// else, if extension mapping
				else if( propValue.startsWith( "*." ))
				{
					// TODO: warn on duplicate patterns?
					extensionServletMappings.put(
						propValue.substring( 2 ), servletName);
				}
				// else, if exact or default mapping
				else if( propValue.startsWith( "/" ))
				{
					if( propValue.length() == 1 )
					{
						defaultServletName = servletName;
					}
					// TODO: warn on duplicate patterns?
					exactServletMappings.put( propValue, servletName);
				}
				else
				{
					log( "Invalid servlet mapping: " +
						propName + "=" + propValue );
				}
			}
		}

		if( defaultServletName == null )
		{
			defaultServletName = DEFAULT_SERVLET_NAME;
		}
	}

	/**
	 * Determines servlet path and name based on request path.
	 * Follows procedure described in section 10.1 of the
	 * Java Servlet Specification 2.2.
	 *
	 * @param path The request path (minus any fragment or query string).
	 * @param servletpathAndName is a String[] with a minimum lenth of 2.
	 */
	protected void getServletPathAndName(
		String path, String[] servletPathAndName )
	{
		String servletPath = "/";
		String servletAlias = defaultServletName;

		// Exact match?
		if( exactServletMappings.containsKey( path ))
		{
			servletPath = path;
			servletAlias = (String)exactServletMappings.get( path );
		}
		// Exact prefix match?
		else if( prefixServletMappings.containsKey( path ))
		{
			servletPath = path;
			servletAlias = (String)prefixServletMappings.get( path );
		}
		else
		{
			// Look for longest prefix match
			int matchLength = 0;
			Enumeration prefixEnum = prefixServletMappings.keys();
			while( prefixEnum.hasMoreElements() )
			{
				String prefix = (String)prefixEnum.nextElement();
				if( path.startsWith( prefix )
				&&  path.charAt( prefix.length() ) == '/'
				&&  prefix.length() > matchLength )
				{
					servletPath = prefix;
					servletAlias = (String)prefixServletMappings.get( prefix );
					matchLength = prefix.length();
				}
			}

			// If no prefix match found
			if( matchLength == 0 )
			{
				// Get extension
				int lastDot = path.lastIndexOf( '.' );
				if( lastDot > -1 )
				{
					String ext = path.substring( lastDot + 1 );
					if( extensionServletMappings.containsKey( ext ))
					{
						servletPath = path;
						servletAlias =
							(String)extensionServletMappings.get( ext );
					}
				}
			}
		}

		servletPathAndName[0] = servletPath;
		servletPathAndName[1] =
			servletProperties.getProperty(
				servletAlias + ".name", servletAlias );
	}

	// TODO: Move to utility class?
	protected static boolean matchURL( String pattern, String path)
	{
		if( pattern.length() == 0)
		{
			return path.length() == 0;
		}
		else if( pattern.indexOf('*') < 0)
		{
			return path.equals(pattern);
		}

		StringTokenizer tokenizer = new StringTokenizer(pattern, "*");
		String fragment = "";
		int pos = 0;

		// the path must start with the first fragment if the
		// pattern doesn't start with a wildcard
		if( !pattern.startsWith("*"))
		{
			fragment = tokenizer.nextToken();
			if( !path.startsWith(fragment))
			{
				return false;
			}
			// return now if no more tokens are present
			if( !tokenizer.hasMoreTokens()) {
				return true;
			}
			// update the current position for the
			// following loop
			pos = fragment.length();
		}

		// loop through the fragments and make sure, that each
		// fragment occurs somewhere _after_ the previous one
		while ( tokenizer.hasMoreTokens() )
		{
			fragment = tokenizer.nextToken();
			if( pos >= path.length())
			{
				return false;
			}
			pos = path.indexOf( fragment, pos);
			if( pos < 0)
			{
				return false;
			}
			pos += fragment.length();
		}

		// the path must end with the last fragment if the
		// pattern doesn't end with a wildcard
		if( !pattern.endsWith("*") && !path.endsWith(fragment))
		{
			return false;
		}

		// all tests passed -> the path matches the pattern
		return true;
	}


	protected Servlet getServletFromName( String servletName)
	{
		String servletClassName = getServletProperty(
			servletName + CODE, servletName);

		Class servletClass;
		try { servletClass = Class.forName( servletClassName); }
		catch( ClassNotFoundException cnfe)
		{
			log( "Class '" + servletClassName + "' not found.");
			return null;
		}

		// If not a servlet
		if( !Servlet.class.isAssignableFrom( servletClass))
		{
			log( servletClassName + " is not a Servlet.");
			return null;
		}

		Servlet servlet = null;
		try { servlet = (Servlet)servletClass.newInstance(); }
		catch( Exception e)
		{
			log( "Could not instantiate Servlet " + servletClassName);
			return null;
		}

		ServletConfig servletConfig =
			new com.smartsc.http.ServletConfig( this,
				getServletProperty( servletName + INIT_ARGS ),
				servletName );

		try { servlet.init( servletConfig); }
		catch( Exception e)
		{
			log( "Error initializing Servlet " + servletClassName);
			log( e);
			return null;
		}

		return servlet;
	}

	public static
	String
	getContentTypeFor( String fileName)
	{
		String type = "text/plain";

		int lastDot = fileName.lastIndexOf( '.');
		if( lastDot != -1)
		{
			String ext = fileName.substring( lastDot);
			String t = (String)extToMimeType.get( ext);
			if( t != null) type = t;
		}

		return type;
	}

	protected
	boolean
	isLogOversized( File log)
	{
		boolean oversized = false;

		if( log != null && log.length() >
			props.getIntProperty( PROP_MAX_LOG_SIZE, DEFAULT_MAX_LOG_SIZE) )
		{
			oversized = true;
		}

		return oversized;
	}

	protected
	void
	mailLog( File log)
	throws IOException
	{
		if( mailHeaders == null)
		{
			mailHeaders = new Hashtable();
			mailHeaders.put(
				Mailer.HEADER_TO, props.get( PROP_MAIL_TO));
			mailHeaders.put(
				Mailer.HEADER_FROM, props.get( PROP_MAIL_FROM));
		}
		mailHeaders.put(
			Mailer.HEADER_SUBJECT, serverName + " " + log.getName());
		try
		{
			// Mail log
			Mailer.send( mailHeaders, log);
		}
		// If bad address
		catch( MalformedURLException mfue)
		{
			// Don't send log anymore
			props.remove( PROP_MAIL_TO);
			throw mfue;
		}
	}

	public
	void
	banner( boolean verbose)
	{
		synchronized( logWriter)
		{
			log( SERVER_INFO);
			log( COPYRIGHT);
			logWriter.println();
			if( verbose)
				props.list( logWriter);
		}
	}

	public static
	void
	help()
	{
		System.out.println( SERVER_INFO);
		System.out.println( COPYRIGHT);
		System.out.println();
		System.out.println( "HttpServer [-(h|v)] [properties_file]");
	}

	public static
	Properties
	getProperties( String[] args)
	{
		boolean help = false;
		boolean verbose = false;

		String propertyFile = null;

		Properties props = new Properties( defaultProps);

		if( args.length > 2)
		{
			help = true;
		}
		else if( args.length == 1)
		{
			if( "-h".equals( args[0])) help = true;
			else if( "-v".equals( args[0])) verbose = true;
			else propertyFile = args[0];
		}
		else if( args.length == 2)
		{
			propertyFile = args[1];
			if( "-v".equals( args[0])) verbose = true;
			else help = true;
		}

		if( help)
		{
			help();
			return null;
		}

		if( propertyFile != null)
		{
			try
			{
				File f = AbsoluteFile.create( propertyFile);
				props.load( new FileInputStream( f));
			}
			catch( IOException ioe)
			{
				System.err.print( "Error reading property file '");
				System.err.print( propertyFile);
				System.err.println( "'.  Using defaults.");
			}
		}

		if( verbose)
		{
			// put() works with both JDK 1.1 and 1.2.
			// setProperty() works with JDK 1.2 only.
			props.put( PROP_VERBOSE, "true");
		}

		// Reconcile path-like properties
		reconcilePathProperty( props, PROP_MIME_TYPES_FILE);
		reconcilePathProperty( props, PROP_DOC_ROOT);
		reconcilePathProperty( props, PROP_SERVLET_PROPS);

		// Load MIME types
		String mimeTypesFilename = props.getProperty( PROP_MIME_TYPES_FILE);
		if( mimeTypesFilename != null)
		{
			try
			{
				extToMimeType.load( new FileInputStream( mimeTypesFilename));
			}
			catch( IOException ioe)
			{
				System.err.print( "Error reading MIME types file '");
				System.err.print( mimeTypesFilename);
				System.err.println( "'.  Using defaults.");
			}
		}

		return props;
	}

	public static
	void
	reconcilePathProperty( Properties props, String pathPropName)
	{
		String pathPropValue = props.getProperty( pathPropName);
		if( pathPropValue == null) return;
		File f = AbsoluteFile.create( pathPropValue);
		pathPropValue = f.getAbsolutePath();
		props.put( pathPropName, pathPropValue);
	}

	public static
	void
	main( String[] args)
	{
		Properties props = getProperties( args);

		if( props != null)
		{
			try
			{
				HttpServer server = new HttpServer( props);
				new Thread( server).start();
			}
			catch( IOException ioe)
			{
				System.err.println( "Could not start server.  (" +
					ioe.getMessage() + ")" );
			}
		}
	}

	private long lastGCTime = System.currentTimeMillis();

	// Server non-properties related constants
	public static final String SERVER_INFO             = "SSC Web Server/1.0";
	public static final String COPYRIGHT =
		"Copyright (C) 1999-2002  Smart Software Consulting.";
	public static final String HTTP_VERSION            = "HTTP/1.1";
	public static final int JSDK_MAJOR_VERSION         = 2;
	public static final int JSDK_MINOR_VERSION         = 2;
	public static final long MIN_GC_TIME               = 30 * 1000;

	// Server properties related constants
	public static final int     DEFAULT_BUFFER_SIZE          = 512;
	public static final String  DEFAULT_DOC_ROOT             = "/docs";
	public static final int     DEFAULT_PORT                 = 80;
	public static final int     DEFAULT_MAX_HANDLERS         = 5;
	public static final int     DEFAULT_MAX_LOG_SIZE         = 10000;
	public static final int     DEFAULT_REQUEST_TIMEOUT      = 10;
	public static final boolean DEFAULT_STACK_TRACE          = false;

	// Servlet constants
//	public static final String DEFAULT_SERVLET_PROPS   = "servlets.properties"; // TINI Omit
	public static final String DEFAULT_SERVLET_PROPS   = "/etc/servlets.props"; // TINI Include

	// Session constants
	public static final int    SESSION_TYPE_COOKIE     = 0;
	public static final int    SESSION_TYPE_URL        = 1;
	public static final int    SESSION_TYPE_NONE       = 2;
	public static final String SESSION_TYPES[]         = { "Cookie", "URL" };
	public static final String DEFAULT_SERVLET_NAME    = "com.smartsc.http.DefaultServlet";
	public static final String DEFAULT_SESSION_NAME    = "JSESSIONID";
	public static final int    DEFAULT_SESSION_TIMEOUT = 60*60;  // 1 hour
	public static final String DEFAULT_SESSION_TYPE    = SESSION_TYPES[0];

	// Server-related property names
	public static final String PROP_BACKLOG         = "server.backlog";
	public static final String PROP_BUFFER_SIZE     = "server.bufferSize";
	public static final String PROP_DOC_ROOT        = "server.docRoot";
	public static final String PROP_INIT_PARAMS     = "server.initParams";
	public static final String PROP_LOG_FILE        = "server.logFile";
	public static final String PROP_MAIL_FROM       = "server.mail.from";
	public static final String PROP_MAIL_TO         = "server.mail.to";
	public static final String PROP_MAX_HANDLERS    = "server.maxHandlers";
	public static final String PROP_MAX_LOG_SIZE    = "server.maxLogSize";
	public static final String PROP_MIME_TYPES_FILE = "server.mimeTypesFile";
	public static final String PROP_PORT            = "server.port";
	public static final String PROP_REQUEST_TIMEOUT = "server.requestTimeout";
	public static final String PROP_STACK_TRACE     = "server.stackTrace";
	public static final String PROP_TRANSFER_LOG    = "server.transferLog";
	public static final String PROP_VERBOSE         = "server.verbose";

	// Servlet-related property names
	public static final String PROP_SERVLET_PROPS   = "servlet.propFile";

	// Session-related property names
	public static final String PROP_SESSION_NAME    = "session.name";
	public static final String PROP_SESSION_TIMEOUT = "session.timeout";
	public static final String PROP_SESSION_TYPE    = "session.type";

	protected Properties props;
	protected static Properties defaultProps = new Properties();
	static
	{
		// Server default properties
		defaultProps.put(
			PROP_BUFFER_SIZE, String.valueOf( DEFAULT_BUFFER_SIZE));
		defaultProps.put(
			PROP_DOC_ROOT, DEFAULT_DOC_ROOT);
		defaultProps.put(
			PROP_MAX_HANDLERS, String.valueOf( DEFAULT_MAX_HANDLERS));
		defaultProps.put(
			PROP_REQUEST_TIMEOUT, String.valueOf( DEFAULT_REQUEST_TIMEOUT));
		defaultProps.put(
			PROP_STACK_TRACE, String.valueOf( DEFAULT_STACK_TRACE));
		defaultProps.put(
			PROP_PORT, String.valueOf( DEFAULT_PORT));
		// Servlet default properties
		defaultProps.put(
			PROP_SERVLET_PROPS, DEFAULT_SERVLET_PROPS);
		// Session default properties
		defaultProps.put(
			PROP_SESSION_TYPE, String.valueOf( DEFAULT_SESSION_TYPE));
		defaultProps.put(
			PROP_SESSION_NAME, DEFAULT_SESSION_NAME);
		defaultProps.put(
			PROP_SESSION_TIMEOUT, String.valueOf( DEFAULT_SESSION_TIMEOUT));
	}

	protected int getBufferSize()
	{
		return props.getIntProperty( PROP_BUFFER_SIZE);
	}

	protected String getDocRoot()
	{
		return props.getProperty( PROP_DOC_ROOT);
	}

	protected boolean isMailingOversizedLogs()
	{
		return props.getProperty( PROP_MAIL_TO) != null;
	}

	protected boolean isStackTraceEnabled()
	{
		return props.getBooleanProperty( PROP_STACK_TRACE);
	}

	protected int getSessionType()
	{
		String sessionTypeName = props.getProperty( PROP_SESSION_TYPE);
		for( int i = 0; i < SESSION_TYPES.length; ++i)
		{
			if( SESSION_TYPES[i].equalsIgnoreCase( sessionTypeName))
				return i;
		}

		return SESSION_TYPE_NONE;
	}

	protected String getSessionName()
	{
		return props.getProperty( PROP_SESSION_NAME);
	}

	protected int getSessionTimeout()
	{
		return props.getIntProperty( PROP_SESSION_TIMEOUT);
	}

	private File logFile;
	private PrintWriter logWriter;
	private File transferLogFile;
	private PrintWriter transferLogWriter;
	private Hashtable mailHeaders;

	// TODO Support https?
	public static final String DEFAULT_SCHEME = "http";

	private ServerSocket serverSocket;
	private String serverName;

	private int handlerCount;
	private int maxHandlers  = DEFAULT_MAX_HANDLERS;

	// String attributeName --> String attributeValue
	private Hashtable attributes = new Hashtable();

	// String servletName --> NamedDispatcher
	private Hashtable namedDispatchers = new Hashtable();

	// servlets.properties stuff
	public static final String CODE      = ".code";
	public static final String INIT_ARGS = ".initArgs";
	public static final String PRELOAD   = ".preload";
	public static final String MAPPING   = ".mapping";
	private final Properties servletProperties = new Properties();
	protected String getServletProperty( String key)
	{
		return servletProperties.getProperty( key);
	}
	protected String getServletProperty( String key, String defaultValue)
	{
		String s = servletProperties.getProperty( key);
		if( s == null) s = defaultValue;
		return s;
	}

	// Mappings
	protected Hashtable exactServletMappings = new Hashtable();
	protected Hashtable prefixServletMappings = new Hashtable();
	protected Hashtable extensionServletMappings = new Hashtable();
	protected String defaultServletName = DEFAULT_SERVLET_NAME;

	// Use java.util.Properties instead of com.smartsc.util.Properties
	// because we don't need the fancy getter methods of the latter.
	private static java.util.Properties
		extToMimeType = new java.util.Properties();
	static
	{
		extToMimeType.put( ".class", "application/octet-stream");
		extToMimeType.put( ".css",   "text/css");
		extToMimeType.put( ".htm",   "text/html");
		extToMimeType.put( ".html",  "text/html");
		extToMimeType.put( ".gif",   "image/gif");
		extToMimeType.put( ".gz",    "application/octet-stream");
		extToMimeType.put( ".jar",   "application/octet-stream");
		extToMimeType.put( ".jpg",   "image/jpeg");
		extToMimeType.put( ".jpeg",  "image/jpeg");
		extToMimeType.put( ".js",    "application/x-javascript");
		extToMimeType.put( ".tini",  "application/octet-stream");
		extToMimeType.put( ".wml",   "text/vnd.wap.wml");
		extToMimeType.put( ".zip",   "application/octet-stream");
	}

	// Use java.util.Properties instead of com.smartsc.util.Properties
	// because we don't need the fancy getter methods of the latter.
	private java.util.Properties
		servletContextInitParams = new java.util.Properties();

	// From javax.servlet.ServletContext
	public Object getAttribute(String name)
	{
		return attributes.get( name);
	}
	public Enumeration getAttributeNames()
	{
		return attributes.keys();
	}
	public ServletContext getContext(String uripath)
	{
		return null;
	}
	public String getInitParameter( String name)
	{
		return (String)servletContextInitParams.get( name);
	}
	public Enumeration getInitParameterNames()
	{
		return servletContextInitParams.keys();
	}
	public int getMajorVersion()
	{
		return JSDK_MAJOR_VERSION;
	}
	public String getMimeType(String file)
	{
		return getContentTypeFor( file);
	}
	public int getMinorVersion()
	{
		return JSDK_MINOR_VERSION;
	}
	public String getRealPath(String path)
	{
		StringBuffer realPath = new StringBuffer( getDocRoot());

		if( path == null)
			return null;

		// TODO path aliases
		// TODO prevent .. from going above docRoot
		if( !path.startsWith( "/"))
		{
			realPath.append( "/");
		}
		realPath.append( path);
		return realPath.toString();
	}
	public RequestDispatcher getRequestDispatcher(String urlPath)
	{
		// Separate query string
		String queryString = null;
		int qryIdx = urlPath.indexOf( '?' );
		if( qryIdx != -1 )
		{
			queryString = urlPath.substring( qryIdx + 1 );
			urlPath = urlPath.substring( 0, qryIdx );
		}

		// Get servlet path and name
		String[] servletPathAndName = new String[2];
		getServletPathAndName( urlPath, servletPathAndName );

		// Get NamedDispatcher
		NamedRequestDispatcher nd = (NamedRequestDispatcher)
			getNamedDispatcher( servletPathAndName[1] );

		if( nd == null )
		{
			return null;
		}

		// Get path info portion of request path
		String pathInfo = null;
		int servletPathLength = servletPathAndName[0].length();
		if( servletPathLength < urlPath.length() )
		{
			pathInfo = urlPath.substring( servletPathLength );
		}

		return new PathRequestDispatcher(
			nd, urlPath, servletPathAndName[0], pathInfo, queryString );
	}
	public RequestDispatcher getNamedDispatcher( String name )
	{
		NamedRequestDispatcher namedDispatcher = null;

		synchronized( namedDispatchers )
		{
			// Is it already loaded?
			namedDispatcher = (NamedRequestDispatcher)
				namedDispatchers.get( name );

			// If not already loaded
			if( namedDispatcher == null)
			{
				Servlet servlet = getServletFromName( name);
				if( servlet != null )
				{
					namedDispatcher =
						new NamedRequestDispatcher( name, servlet );
					namedDispatchers.put( name, namedDispatcher );
				}
			}
		}
		return namedDispatcher;
	}
	/* TODO.17
	protected RequestDispatcher getRequestDispatcher(
		Servlet servlet, String urlpath)
	{
		if( servlet == null)
			return null;

		RequestDispatcher rd = new
			com.smartsc.http.RequestDispatcher( servlet, urlpath);

		return rd;
	}
	*/

	public URL getResource(String path)
	throws MalformedURLException
	{
		// TODO Support URL resources
		return null;
	}

	public InputStream getResourceAsStream(String path)
	{
		// Build filename
		String filename = getRealPath( path);

		// Construct new file object
		File file = new File( filename);

		// If file does not exist
		// or can not be read
		// or is a directory
		if( !file.exists()
		||  !file.canRead()
		||  file.isDirectory() )
		{
			return null;
		}

		// Get length
		long length = file.length();

		// If file does not exist
		// or can not be read
		// or has zero length
		if( !file.exists()
		||  !file.canRead()
		||  length == 0 )
		{
			return null;
		}

		FileInputStream in = null;
		try { in = new FileInputStream(file); }
		catch( FileNotFoundException fnfe) {}

		return in;
	}
	public String getServerInfo()
	{
		return SERVER_INFO;
	}
	/** @deprecated */
	public Servlet getServlet(String name)
	{
		return null;
	}
	/** @deprecated */
	public Enumeration getServletNames()
	{
		return new Vector(0).elements();
	}
	/** @deprecated */
	public Enumeration getServlets()
	{
		return new Vector(0).elements();
	}
	/** @deprecated */
	public void log( Exception exception, String msg)
	{
		log( msg, exception);
	}
	public void log( String msg)
	{
		// TODO Add timestamp
		if( msg != null && msg.length() > 0)
		{
			synchronized( logWriter)
			{
				logWriter.println( msg);
			}
		}
	}
	public void log( String msg, Throwable throwable)
	{
		synchronized( logWriter)
		{
			log( msg);
			if( isStackTraceEnabled())
			{
				throwable.printStackTrace( logWriter);
			}
			else
			{
				// TODO Indicate that stack tracing is disabled?
				log( throwable.toString() );
			}
		}
	}
	public void log( Throwable t)
	{
		log( t.getClass().getName() + ": " + t.getMessage(), t);
	}
	public void removeAttribute(String name)
	{
		attributes.remove( name);
	}
	public void setAttribute(String name, Object o)
	{
		attributes.put( name, o);
	}
}
